import util from 'util';
// const _ = '_';
export const $ = (element = '') => document.querySelector(element);
export const _range = (length: number, callback = (_: any, i: number) => i) => Array.from({ length },
  callback);
// const curry = fn => (...args) => fn.bind(null, ...args);

export const curry = fn => function $curry(...args) {
  return args.length < fn.length
    ? $curry.bind(null, ...args)
    : fn.call(null, ...args);
};

export const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];

export const reduce = curry((fn, zero, xs) => xs.reduce(fn, zero));

export const toUpper = s => s.toUpperCase();

export const toLower = s => s.toLowerCase();

export const reverse = xs => Array.isArray(xs) ? xs.reverse() : xs.split('').reverse().join('');

export const replace = curry((re, rpl, str) => str.replace(re, rpl));

export const head = xs => xs[0];

export const last = xs => xs[xs.length - 1];

export const intercalate = curry((str, xs) => xs.join(str));

export const map = curry((fn, f) => f.map(fn));

export const split = curry((sep, str) => str.split(sep));

export const prop = curry((p, obj) => obj[p]);

export const match = curry((re, s) => re.test(s));

export const add = curry((a, b) => a + b);

export const inspect = (x) => {
  if (x && typeof x.inspect === 'function') {
    return x.inspect();
  }

  function inspectFn(f) {
    return f.name ? f.name : f.toString();
  }

  function inspectTerm(t) {
    switch (typeof t) {
      case 'string':
        return `'${t}'`;
      case 'object': {
        const ts = Object.keys(t).map(k => [k, inspect(t[k])]);
        return `{${ts.map(kv => kv.join(': ')).join(', ')}}`;
      }
      default:
        return String(t);
    }
  }

  function inspectArgs(args) {
    return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args);
  }

  return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x);
};

export const concat = curry((a, b) => a.concat(b));

export const flip = curry((fn, a, b) => fn(b, a));

export const append = flip(concat);

export const identity = x => x;

export class Maybe<T> {
  private readonly $value: T;

  get isNothing() {
    return this.$value === null || this.$value === undefined;
  }

  get isJust() {
    return !this.isNothing;
  }

  constructor(x: T) {
    this.$value = x;
  }

  private [util.inspect.custom]() {
    return this.isNothing ? 'Nothing' : `Just(${inspect(this.$value)})`;
  }

  // ----- Pointed Maybe
  public static of(x) {
    return new Maybe(x);
  }

  // ----- Functor Maybe
  public map(fn) {
    return this.isNothing ? this : Maybe.of(fn(this.$value));
  }

  // ----- Applicative Maybe
  public ap(f) {
    return this.isNothing ? this : f.map(this.$value);
  }

  // ----- Monad Maybe
  public chain(fn) {
    return this.map(fn).join();
  }

  public join() {
    return this.isNothing ? this : this.$value;
  }

  // ----- Traversable Maybe
  public sequence(of) {
    return this.traverse(of, identity);
  }

  public traverse(of, fn) {
    return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of);
  }
}

export const safeHead = xs => Maybe.of(xs[0]);

export const maybe = curry((v, f, m) => m.isNothing ? v : f(m.$value));

export const nothing = Maybe.of(null);

export class Either<T> {
  protected readonly $value: T;

  constructor(x: T) {
    this.$value = x;
  }

  public static of<T>(x: T) {
    return new Right(x);
  }
}

export class Right<T> extends Either<T> {
  get isLeft() {
    return false;
  }

  get isRight() {
    return true;
  }

  public [util.inspect.custom]() {
    return `Right(${inspect(this.$value)})`;
  }

  public map(fn: (x: T) => T) {
    return Either.of(fn(this.$value));
  }

  public ap(f) {
    return f.map(this.$value);
  }

  public chain(fn: (x: T) => T) {
    return fn(this.$value);
  }

  public join() {
    return this.$value;
  }

  public sequence(of) {
    return this.traverse(of, identity);
  }

  public traverse(of, fn) {
    fn(this.$value).map(Either.of);
  }
}

export class Left<T> extends Either<T> {
  get isLeft() {
    return true;
  }

  get isRight() {
    return false;
  }

  public [util.inspect.custom]() {
    return `Left(${inspect(this.$value)})`;
  }

  // ----- Functor (Either a)
  public map() {
    return this;
  }

  // ----- Applicative (Either a)
  public ap() {
    return this;
  }

  // ----- Monad (Either a)
  public chain() {
    return this;
  }

  public join() {
    return this;
  }

  // ----- Traversable (Either a)
  public sequence(of) {
    return of(this);
  }

  public traverse(of, fn) {
    return of(this);
  }
}

export const left = x => new Left(x);
